package org.pc2r.game;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.pc2r.exception.PC2RException;
import org.pc2r.game.model.Player;
import org.pc2r.game.model.SubMarine;
import org.pc2r.network.SocketReadWriter;
import org.pc2r.protocole.message.Message;
import org.pc2r.util.SystemUtil;

/**
 * Une classe s'occupant d'associer une {@link Socket} à un joueur. Cette classe
 * est propre à chaque {@link Game} et contient donc tous les joueurs de la
 * partie. Elle s'occupe aussi de gérer les duplications de nom.
 * 
 * 
 */
public class Game implements Closeable {

	private final Map<String, SocketReadWriter> players = new ConcurrentHashMap<>(
			4);
	private List<Player> sortedPlayers = new ArrayList<>(4);

	public boolean isFull() {
		return players.size() >= 4;
	}
	
	public Player getLastAlivePlayer() {
	  for(Player player : sortedPlayers) {
      if(false == player.isDead()) {
        return player;
      }
    }
	  return null;
	}
	
	public boolean isFinished() {
	  int size = sortedPlayers.size();
    if(size <= 1) return true;
	  int nbDead = 0;
	  for(Player player : sortedPlayers) {
	    if(player.isDead()) nbDead++;
	  }
	  return nbDead >= (size - 1);
	}

	public List<SubMarine> getSubmarines() {
		List<SubMarine> subs = new ArrayList<>();
		for (Player play : sortedPlayers)
			subs.addAll(play.getSubmarines());
		return subs;
	}

	public boolean canContinue() {
		return players.size() > 1 && isFull() == false;
	}

	public String addPlayer(String name, Socket socket) throws PC2RException {
		return addPlayer(name, new SocketReadWriter(socket));
	}

	/**
	 * Ajoute un client dont le nom est spécifié avec la {@link Socket}
	 * spécifiée.
	 * 
	 * @param name
	 *            {@link String} le nom du client
	 * @param socket
	 *            {@link Socket} la socket du client
	 * @throws PC2RException
	 * @see {@link #subAdd(String, String, Socket, int)}
	 */

	public String addPlayer(String name, SocketReadWriter socket)
			throws PC2RException {
		return subAdd(name, name, socket, 1);
	}

	/**
	 * Envoie un message au client dont le nom est spécifié
	 * 
	 * @param name
	 *            {@link String} le nom du client
	 * @param request
	 *            {@link Message} un message
	 * @throws PC2RException
	 *             Si le client n'existe pas
	 * @throws IOException
	 *             Si une erreur survient durant l'envoi du message
	 */
	public void send(String name, Message request) throws PC2RException,
			IOException {
		assert request != null;
		SocketReadWriter socket = get(name);
		if (socket == null) {
			throw new PC2RException("Le joueur de nom " + name
					+ " n'existe pas.");
		}
		socket.write(request);
	}

	public SocketReadWriter get(String name) {
		return players.get(name);
	}

	public String read(String name) throws IOException {
		SocketReadWriter socket = get(name);
		if (socket == null) {
			throw new PC2RException("Le joueur de nom " + name
					+ " n'existe pas.");
		}
		return socket.read(true);
	}

	/**
	 * Envoie le message spécifié à tous les clients
	 * 
	 * @param message
	 *            {@link Message} le message à envoyer
	 * @return {@link List} La liste des clients à qui le message <b>n'a pas été
	 *         envoyé.</b>
	 * @throws IOException
	 *             Si une erreur survient durant l'envoi d'un message
	 */
	public List<String> broadcast(Message message) {
		List<String> playersError = null;
		for (String name : players.keySet()) {
			try {
				send(name, message);
			} catch (PC2RException | IOException exception) {
				if (playersError == null)
					playersError = new ArrayList<>(4);
				playersError.add(name);
			}
		}
		return playersError;
	}

	/**
	 * Retourne la liste des joueurs présents
	 * 
	 * @return {@link List} la liste des joueurs présents
	 */
	public List<String> players() {
		return new ArrayList<>(players.keySet());
	}

	/**
	 * Déconnecte le client dont le nom est spécifié
	 * 
	 * @param name
	 *            {@link Stirng} le nom d'un client
	 * @return {@code boolean} vrai si la partie doit se terminer
	 */
	public boolean remove(String name) {
		SocketReadWriter socket = players.remove(name);
		SystemUtil.close(socket);
		if (sortedPlayers != null) {
			sortedPlayers.remove(name);
		}
		return canContinue();
	}

	public int size() {
		return players.size();
	}

	@Override
	public void close() {
		closeExcept(new ArrayList<>(players.keySet()));
	}
	
	public void closeExcept(List<String> list) {
	  for(String name : players.keySet()) {
	    if(false == list.contains(name)) {
	      remove(name);
	    }	    
	  }
	  // Si plus aucun joueur, on vide tout
	  if(players.isEmpty()) {
	    sortedPlayers.clear();
	  }
	}

	private String subAdd(String realName, String name,
			SocketReadWriter socket, int i) throws PC2RException {
		if (i >= 50 || i < 0)
			throw new PC2RException();
		// Si on met 2 noms de joueurs identiques, o ajoute i
		// "nom -> nom0 -> nom1"
		if (players.containsKey(name)) {
			name = realName + i;
			return subAdd(realName, name, socket, ++i);
		}
		players.put(name, socket);
		return name;
	}

	public List<Player> getSortedPlayers() {
		return sortedPlayers;
	}

	public void addAllPlayers(List<Player> sortedPlayers) {
		this.sortedPlayers.addAll(sortedPlayers);
	}
}
